//@version=6
indicator("Strong Volume Map", overlay = true, max_boxes_count = 40, max_lines_count = 10, max_labels_count = 20, max_bars_back = 1100, behind_chart = true)

const int K    = 10
const int BINS = 240

gD = "═══ Display ═══"
lookbackIn = input.int(300, "Lookback (bars)", minval = 50, maxval = 1000, group = gD)
extendIn   = input.int(30, "Extend Right (bars)", minval = 0, maxval = 200, group = gD)
showInfo   = input.bool(true, "Show Strength Labels", group = gD)
labSizeS   = input.string("Small", "Label Size", options = ["Tiny", "Small", "Normal", "Large"], group = gD)
labMinSc   = input.float(0.0, "Label Min Score", minval = 0, maxval = 10, step = 0.5, group = gD)

gF = "═══ Exhaustion Fades ═══"
showSig    = input.bool(true, "Show Fade Signals", group = gF)
sigLenIn   = input.int(100, "Fair Value Length", minval = 10, maxval = 500, group = gF)
sigBandIn  = input.float(2.0, "Stretch (σ)", minval = 0.5, maxval = 5.0, step = 0.1, group = gF)
sigVolZIn  = input.float(1.5, "Volume Climax (z)", minval = 0.0, maxval = 6.0, step = 0.1, group = gF)
sigWickIn  = input.float(0.30, "Min Rejection Wick", minval = 0.0, maxval = 0.9, step = 0.05, group = gF)
sigCoolIn  = input.int(5, "Cooldown Bars", minval = 0, maxval = 200, group = gF)

gC = "═══ Style ═══"
supCol     = input.color(#21c997, "Bullish Tone (Support)", group = gC)
resCol     = input.color(#cc24e2, "Bearish Tone (Resistance)", group = gC)
liveCol    = input.color(#9AA0A6, "In-Play Tone", group = gC)
fillTransp = input.int(84, "Layer Transparency", minval = 50, maxval = 96, group = gC)
showBord   = input.bool(true, "Show Layer Borders", group = gC)
bordTransp = input.int(35, "Border Transparency", minval = 0, maxval = 100, group = gC)
showShade  = input.bool(true, "Bed Shading", group = gC)

gA = "═══ Alerts ═══"
instThr    = input.float(7.0, "Strong Layer Score", minval = 0, maxval = 10, step = 0.5, group = gA)

TRANSP = color.new(#000000, 100)

f_size(s) =>
    switch s
        "Tiny"   => size.tiny
        "Small"  => size.small
        "Normal" => size.normal
        "Large"  => size.large
        => size.normal

f_stars(sc) => sc >= 8 ? "★★★★" : sc >= 6.5 ? "★★★" : sc >= 5 ? "★★" : sc >= 3 ? "★" : "·"
f_tier(sc)  => sc >= 8 ? "ELITE" : sc >= 6.5 ? "STRONG" : sc >= 5 ? "MODERATE" : sc >= 3 ? "WEAK" : "THIN"

float vol    = nz(volume) > 0 ? volume : 1.0
float sumVol = math.sum(vol, sigLenIn)
float fv     = math.sum(hlc3 * vol, sigLenIn) / sumVol
float fv2    = math.sum(hlc3 * hlc3 * vol, sigLenIn) / sumVol
float fvSd   = math.sqrt(math.max(fv2 - fv * fv, 0.0))
float volSd  = ta.stdev(vol, 50)
float volZ   = volSd > 0 ? (vol - ta.sma(vol, 50)) / volSd : 0.0
float rngBar = high - low
float upWick = rngBar > 0 ? (high - math.max(open, close)) / rngBar : 0.0
float dnWick = rngBar > 0 ? (math.min(open, close) - low) / rngBar : 0.0
float clv    = rngBar > 0 ? (close - low) / rngBar : 0.5
float strUp  = fvSd > 0 ? (high - fv) / fvSd : 0.0
float strDn  = fvSd > 0 ? (fv - low) / fvSd : 0.0

var int lastSellBar = -100000
var int lastBuyBar  = -100000
bool sellSig = showSig and barstate.isconfirmed and strUp >= sigBandIn and volZ >= sigVolZIn and upWick >= sigWickIn and clv <= 0.6 and bar_index - lastSellBar > sigCoolIn
bool buySig  = showSig and barstate.isconfirmed and strDn >= sigBandIn and volZ >= sigVolZIn and dnWick >= sigWickIn and clv >= 0.4 and bar_index - lastBuyBar > sigCoolIn
if sellSig
    lastSellBar := bar_index
if buySig
    lastBuyBar := bar_index

plotshape(buySig, "Fade Long", shape.triangleup, location.belowbar, supCol, size = size.tiny)
plotshape(sellSig, "Fade Short", shape.triangledown, location.abovebar, resCol, size = size.tiny)

var array<box>   bedBx   = array.new<box>()
var array<box>   shadeBx = array.new<box>()
var array<label> tagLb   = array.new<label>()
var bool nearStrongSup = false
var bool nearStrongRes = false

if barstate.islast
    if bedBx.size() == 0
        for k = 0 to K - 1
            bedBx.push(box.new(bar_index, close, bar_index, close, border_color = TRANSP, border_width = 1, bgcolor = TRANSP))
            shadeBx.push(box.new(bar_index, close, bar_index, close, border_color = TRANSP, border_width = 0, bgcolor = TRANSP))
            tagLb.push(label.new(bar_index, close, "", style = label.style_label_left, color = TRANSP))
    nearStrongSup := false
    nearStrongRes := false

    int n = math.min(lookbackIn, bar_index + 1)
    float rHi = high
    float rLo = low
    for i = 0 to n - 1
        rHi := math.max(rHi, high[i])
        rLo := math.min(rLo, low[i])

    if n >= 20 and rHi > rLo
        float step = (rHi - rLo) / BINS
        array<float> pv = array.new<float>(BINS, 0.0)
        for i = 0 to n - 1
            float h = high[i]
            float l = low[i]
            float v = nz(volume[i]) > 0 ? volume[i] : 1.0
            float r = h - l
            int b0 = math.max(0, math.min(BINS - 1, int((l - rLo) / step)))
            int b1 = math.max(0, math.min(BINS - 1, int((h - rLo) / step)))
            for j = b0 to b1
                float bl = rLo + j * step
                float sh = r > 0 ? math.max(0.0, math.min(h, bl + step) - math.max(l, bl)) / r : 1.0 / (b1 - b0 + 1)
                pv.set(j, pv.get(j) + v * sh)

        float tot = pv.sum()
        array<float> edges = array.new<float>(K + 1, rLo)
        edges.set(K, rHi)
        float cum = 0.0
        int q = 1
        for j = 0 to BINS - 1
            float w = pv.get(j)
            while q < K and cum + w >= tot * q / K
                float frac = w > 0 ? (tot * q / K - cum) / w : 0.0
                edges.set(q, rLo + (j + frac) * step)
                q += 1
            cum += w

        float avgTh = (rHi - rLo) / K
        array<float> ths = array.new<float>(K, 0.0)
        for k = 0 to K - 1
            ths.set(k, math.max(edges.get(k + 1) - edges.get(k), syminfo.mintick))
        int x0 = bar_index - n + 1
        int x1 = bar_index + extendIn
        float minGap = (rHi - rLo) * 0.028
        float lastY = na
        float lastSc = na
        int lastK = -1
        for k = 0 to K - 1
            float bot = edges.get(k)
            float top = edges.get(k + 1)
            float th  = ths.get(k)
            int thicker = 0
            for m = 0 to K - 1
                if ths.get(m) > th
                    thicker += 1
            float absSc = math.min(10.0, math.max(0.0, (math.log(avgTh / th) / math.log(2.0) + 1.5) / 3.5 * 10.0))
            float sc  = 0.5 * absSc + 0.5 * 10.0 * thicker / (K - 1)
            bool isSup  = top < close
            bool isRes  = bot > close
            color tone  = isSup ? supCol : isRes ? resCol : liveCol
            string side = isSup ? "Support" : isRes ? "Resistance" : "In Play"
            int tr = int(math.max(50, math.min(96, fillTransp + 6 - sc * 1.2)))
            box b = bedBx.get(k)
            b.set_lefttop(x0, top)
            b.set_rightbottom(x1, bot)
            b.set_bgcolor(color.new(tone, tr))
            b.set_border_color(showBord ? color.new(tone, bordTransp) : TRANSP)
            box s = shadeBx.get(k)
            s.set_lefttop(x0, bot + (top - bot) * 0.18)
            s.set_rightbottom(x1, bot)
            s.set_bgcolor(showShade ? color.new(tone, math.max(tr - 8, 40)) : TRANSP)
            label t = tagLb.get(k)
            t.set_xy(x1 + 1, (top + bot) / 2)
            t.set_size(f_size(labSizeS))
            float ym = (top + bot) / 2
            bool show = showInfo and sc >= labMinSc
            if show and not na(lastY) and ym - lastY < minGap
                if sc > lastSc
                    tagLb.get(lastK).set_text("")
                else
                    show := false
            if show
                lastY := ym
                lastSc := sc
                lastK := k
                t.set_text(f_stars(sc) + "  " + str.tostring(sc, "#.0") + "/10  " + f_tier(sc) + "  ·  " + side + "  " + str.tostring(bot, format.mintick) + " – " + str.tostring(top, format.mintick))
                t.set_textcolor(tone)
                t.set_tooltip("Layer " + str.tostring(k + 1) + " of " + str.tostring(K) + " · holds 10% of traded volume\nThickness " + str.tostring(th, format.mintick) + " (average " + str.tostring(avgTh, format.mintick) + ")\nThinner layer = denser volume = stronger level")
            else
                t.set_text("")
            if sc >= instThr and low <= top and high >= bot
                if isSup or (not isRes and close >= (top + bot) / 2)
                    nearStrongSup := true
                else
                    nearStrongRes := true

alertcondition(buySig, "Fade Long", "Strong Volume Map: exhaustion fade LONG on {{ticker}} at {{close}}")
alertcondition(sellSig, "Fade Short", "Strong Volume Map: exhaustion fade SHORT on {{ticker}} at {{close}}")
alertcondition(nearStrongSup, "Strong Support Layer Test", "Strong Volume Map: price testing a strong support layer on {{ticker}}")
alertcondition(nearStrongRes, "Strong Resistance Layer Test", "Strong Volume Map: price testing a strong resistance layer on {{ticker}}")
